package com.honyelchak.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.honyelchak.shortlink.base.common.convention.exception.ClientException;
import com.honyelchak.shortlink.base.common.convention.exception.ServiceException;
import com.honyelchak.shortlink.base.common.enums.ValidDateTypeEnum;
import com.honyelchak.shortlink.project.dao.entity.ShortLinkDO;
import com.honyelchak.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.honyelchak.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.honyelchak.shortlink.project.dao.mapper.ShortLinkMapper;
import com.honyelchak.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.honyelchak.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.honyelchak.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.honyelchak.shortlink.project.dto.resp.ShortLinkCountQueryRespDTO;
import com.honyelchak.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.honyelchak.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.honyelchak.shortlink.project.service.ShortLinkService;
import com.honyelchak.shortlink.project.util.HashUtil;
import com.honyelchak.shortlink.project.util.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static com.honyelchak.shortlink.project.common.RedisCacheConstant.*;
import static com.honyelchak.shortlink.project.common.ShortLinkConstant.NOT_FOUND_REDIRECT;

@Service
@Slf4j
@AllArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortLinkUrlBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;

    private final RedissonClient redissonClient;
    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 在创建短链接的时候，会进行缓存预热
     *
     * @param reqDTO
     * @return
     */
    @Override
    @Transactional
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO reqDTO) {
        String shortLinkSuffix = generateSuffix(reqDTO);
        ShortLinkDO shortLinkDO = BeanUtil.toBean(reqDTO, ShortLinkDO.class);
        String fullShortLinkUrl = reqDTO.getDomain() + "/" + shortLinkSuffix;
        shortLinkDO.setFullShortUrl(fullShortLinkUrl);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setFavicon(getFavicon(reqDTO.getOriginUrl()));

        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortLinkUrl)
                .gid(reqDTO.getGid())
                .build();
        try {
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(linkGotoDO);
        } catch (DuplicateKeyException ex) {
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getFullShortUrl, fullShortLinkUrl);
            ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
            if (hasShortLinkDO != null) {
                log.warn("短链接：{} 重复入库", fullShortLinkUrl);
                throw new ClientException("该短链接已存在");
            }
        }
        // 将该链接存入redis，有效期设置为30天(缓存预热)
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortLinkUrl),
                shortLinkDO.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), java.util.concurrent.TimeUnit.MILLISECONDS);

        // 由于一个短网址可以在多个域名使用，所以此处加到bloomFilter中的是ShortLink的完整版URL
        shortLinkUrlBloomFilter.add(fullShortLinkUrl);
        return ShortLinkCreateRespDTO.builder()
                .gid(shortLinkDO.getGid())
                .fullShortUrl(shortLinkDO.getShortUri())
                .originUrl(shortLinkDO.getOriginUrl())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO reqDTO) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, reqDTO.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(reqDTO, queryWrapper);
        return resultPage.convert((e) -> BeanUtil.toBean(e, ShortLinkPageRespDTO.class));
    }

    @Override
    public List<ShortLinkCountQueryRespDTO> listGroupShortLinkCount(List<String> gids) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid, count(*) shortLinkCount")
                .in("gid", gids)
                .eq("enable_status", 0)
                .groupBy("gid");

        List<Map<String, Object>> objects = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(objects, ShortLinkCountQueryRespDTO.class);
    }

    @Transactional
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestDTO) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestDTO.getGid())
                .eq(ShortLinkDO::getFullShortUrl, requestDTO.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        // TODO 前端传输一个新旧gid，当要修改gid的时候，先从按照原来的gid查出记录，进行删除，然后重新插入到数据库中。
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(hasShortLinkDO.getDomain())
                .shortUri(hasShortLinkDO.getShortUri())
                .clickNum(hasShortLinkDO.getClickNum())
                .favicon(hasShortLinkDO.getFavicon())
                .createdType(hasShortLinkDO.getCreatedType())
                .gid(requestDTO.getGid())
                .originUrl(requestDTO.getOriginUrl()) // TODO 修改originUrl 实体名字
                .describe(requestDTO.getGid())
                .validDateType(requestDTO.getValidDateType())
                .validDate(requestDTO.getValidDate())
                .build();
        if (Objects.equals(hasShortLinkDO.getGid(), requestDTO.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestDTO.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestDTO.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(ShortLinkDO::getCreatedType, Objects.equals(requestDTO.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()));
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestDTO.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            baseMapper.delete(updateWrapper);
            baseMapper.insert(shortLinkDO);
        }

    }

    /**
     * 短链接到原始链接的转换
     * 处理点：
     * 1. 缓存击穿：利用分布式锁互斥更新(另外尽管没有设置过期时间，但是如果redis是随机更新的，则也有可能发生缓存击穿的)
     * 2. 缓存穿透：将不存在的链接设置空值来判断。
     *
     * @param shortUri
     * @param request
     * @param response
     */
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 原理就是直接让短链接进行跳转。
        String serverName = request.getServerName();

        String fullShortUrl = serverName + "/" + shortUri;

        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 如果redis命中，直接返回
        if (StrUtil.isNotBlank(originalLink)) {
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }
        // 如果redis未命中，查询BloomFilter
        if (!shortLinkUrlBloomFilter.contains(fullShortUrl)) {
            ((HttpServletResponse) response).sendRedirect(NOT_FOUND_REDIRECT);
            return;
        }
        // 避免redis缓存穿透
        String gotoIsNull = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(gotoIsNull)) {
            ((HttpServletResponse) response).sendRedirect(NOT_FOUND_REDIRECT);
            return;
        }
        // 查询数据库
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            // 双重判定锁 避免缓存击穿
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originalLink)) {
                ((HttpServletResponse) response).sendRedirect(NOT_FOUND_REDIRECT);
                return;
            }
            LambdaQueryWrapper<ShortLinkGotoDO> gotoDOQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(gotoDOQueryWrapper);
            if (shortLinkGotoDO == null) {
                // 如果不存在，避免缓存穿透设置空值
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), NULL_VALUE, 30, TimeUnit.SECONDS);
                ((HttpServletResponse) response).sendRedirect(NOT_FOUND_REDIRECT);
                return;
            }
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid());
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), NULL_VALUE, 30, TimeUnit.SECONDS);
                ((HttpServletResponse) response).sendRedirect(NOT_FOUND_REDIRECT);
                return;
            }

            stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
            );
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }


    /**
     * 短链接后缀生成方法
     *
     * @param shortLinkCreateReqDTO
     * @return 短链接字符串
     */
    private String generateSuffix(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {
        String originUrl = shortLinkCreateReqDTO.getOriginUrl();
        int customGenerateCount = 0;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String base62Str = HashUtil.hashToBase62(originUrl);
            if (!shortLinkUrlBloomFilter.contains(base62Str)) {
                LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, shortLinkCreateReqDTO.getDomain() + "/" + originUrl);
                ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
                if (shortLinkDO == null) {
                    return base62Str;
                }
            }
            customGenerateCount++;
        }
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrL = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrL.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            return faviconLink != null ? faviconLink.attr("abs:href") : null;
        }
        return null;
    }
}
